AWS SDK for Javascriptで1000件を超えるオブジェクト削除する際に発生したMalformedXMLエラーの対処

AWS SDK for Javascriptで1000件を超えるオブジェクト削除する際に発生したMalformedXMLエラーの対処

Clock Icon2023.07.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

アノテーション の中野です。

S3内オブジェクト削除をAWS SDK for JavaScript v3のDeleteObjectsCommandを使って行っていました。
しかし、ある日を境にファイル削除でエラーが発生するようになったため、詳細を確認して対応しました。

背景

以前にローンチしたプロダクトで運用がしばらく経つとユーザー数も増えていきました。
このプロダクトでは、S3オブジェクトをLambdaから削除する処理を、深夜帯に動く日時バッチとして実装していました。
しかし、ユーザー数が増加していくことで、S3内のオブジェクト数も比例して増加しました。
結果的に、日時バッチとして削除する対象のオブジェクトが当初想定していたよりも多く、DeleteObjectsCommandの制約に引っかかってしまいました。

結論

  • MalformedXMLエラーは、1000件以上のオブジェクト削除すると発生するエラー
  • AWS SDKのDeleteObjectsCommandは1000件ずつしか削除できないのが仕様
  • DeleteObjectsCommandで1000件を超えるオブジェクト削除する場合は、1000件ごとに分割して削除を実行する

エラー内容

1000件を超えるオブジェクトを削除する際に、以下のようなエラーが発生します。

▼エラーメッセージ

MalformedXML: The XML you provided was not well-formed or did not validate against our published schema

エラー原因

サンプルのコードとして、以下のdeleteAll関数を例に説明します。
この関数の仕様として、引数に削除対象のファイルのpathをもったリストを渡して、関数内でリストに対してDeleteObjectsCommandを実行します。

このとき、pathが1000件以内だとDeleteObjectsCommandの処理は成功します。
しかし、1000件を超える場合は上述のエラーが発生します。

import {
  S3Client,
  DeleteObjectsCommand,
} from "@aws-sdk/client-s3"

const client = new S3Client({
  region: "ap-northeast-1",
})

export interface ObjectProps {
  path: string
}

const deleteAll = async (objects: ObjectProps[]): Promise<void> {
	const objects = object.map((obj) => ({
      Key: obj.path,
  }))
  if (objects.length === 0) return

  const bucketParams = {
    Bucket: bucketName,
    Delete: {
      Objects: objects,
    },
  })
  await client.send(new DeleteObjectsCommand(bucketParams))
}

修正したコード

エラーを解消するためには、1000件ごとにDeleteObjectsCommandを実行すれば成功します。

import {
  S3Client,
  DeleteObjectsCommand,
} from "@aws-sdk/client-s3"

const client = new S3Client({
  region: "ap-northeast-1",
})

export interface ObjectProps {
  path: string
}

const deleteObjectsPerThousand = async (
    targetObjects: {
      Key: string
    }[]
  ): Promise<void> {
    const bucketParams = {
      Bucket: bucketName,
      Delete: {
        Objects: targetObjects,
      },
    })
    await client.send(new DeleteObjectsCommand(bucketParams))
  }

const deleteAll = async (objects: ObjectProps[]): Promise<void>  {
	const objects = object.map((obj) => ({
      Key: obj.path,
  }))
  if (objects.length === 0) return

  // 1000件ごとに分割して配列にいれる
  const totalObjects = []
  while (objects.length > 0) {
    const objectChunk = objects.splice(0, 1000)
    totalObjects.push(objectChunk)
  }

  await Promise.all(
    totalObjects.map((deleteObject) =>
      deleteObjectsPerThousand(deleteObject)
    )
  )
}

上記コードの37行目でspliceを使ってオブジェクトを1000件ごとに配列に分割しています。
その後、deleteObjectを引数にしてdeleteObjectsPerThousand関数を並列に呼び出して削除処理を実行します。
注意点としてPromiss.allを使っていますが、削除対象のオブジェクトの並列処理が数件の場合はよいですが、多すぎるとS3のレートリミットに引っかかる可能性があります。
ただし、今回の仕様では日時バッチが深夜帯に動くことや、オブジェクトの順番に依存性がないことを踏まえて並行で削除する処理としました。

[補足] ListObjectV2で取得した1000件以上の全ファイル削除

今回の例は、日時バッチ処理で発生したエラーの解消について紹介しました。
調査している段階で、ListObjectsV2Commandを利用して1000件以内のオブジェクトを削除する方法はありましたが、1000件を超える場合のスクリプトは見つかりませんでした。

そのため、ListObjectsV2Commandで取得したS3内の全オブジェクトを削除する方法が、同じようにできないか調べてみました。
すると、paginateListObjectsV2という関数を見つけました。
paginateListObjectsV2を利用することで、1000件ごとにページネーション取得したオブジェクトをループしながら削除できます。

ListObjectsV2Commandも1000件までしかオブジェクトを取得できないという制約があるため、この方法を利用すれば、S3内のオブジェクトの全削除はできそうです。

import {
  S3Client,
  DeleteObjectsCommand,
  paginateListObjectsV2,
} from "@aws-sdk/client-s3"

const client = new S3Client({
  region: "ap-northeast-1",
})

const BUCKET_NAME = process.env.BUCKET_NAME || ""

const deleteObjectsPerThousand = async (bucketName: string): Promise<void> => {
  if (bucketName == null || bucketName == "") return

  for await (const data of paginateListObjectsV2(
    { client, pageSize: 1000 },
    { Bucket: bucketName }
  )) {
    const s3Objects = data.Contents?.map(({ Key }) => ({ Key }))
    const bucketParams = { Bucket: bucketName, Delete: { Objects: s3Objects } }
    try {
      console.log(data)
      await client.send(new DeleteObjectsCommand(bucketParams))
    } catch (err) {
      console.error(err)
    }
  }
}

deleteObjectsPerThousand(BUCKET_NAME)
$ npx ts-node ./deleteObjectsPerThousand.ts

さいごに

この記事が誰かのお役に立ちますように

参考情報

アノテーション株式会社について

アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。少しでもご興味あれば、アノテーション株式会社 WEB サイトをご覧ください。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.